/*  GStruct: data structure helpers for Java
 *  Copyright (C) 2011 Giulio Franco
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package gstruct.assoc;

import gstruct.extensions.Mapper;
import gstruct.extensions.Predicate;
import gstruct.extensions.Sequential;

import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * Lazy map implementation that pipes two backing maps.
 * Starting from a Map<K1, K2> m1 and a Map<K2, V> m2,
 * the LazyPipeMap exposes a Map<K1, V> view,
 * where <pre>lazyPipeMap.get(k1).equals(v) if and only if</pre>
 * a key k2 exists so that
 * <pre>m1.get(k1).equals(k2) && m2.get(k2).equals(v)</pre>
 * 
 * This implementation is lazy, which means it will reflect
 * changes in the backing maps, and the iterators it return might
 * throw a {@link ConcurrentModificationException} if the backing maps
 * are modified during iterations.
 * @author Giulio Franco <giulio_franco@msn.com>
 *
 * @param <K1>	Key type of the first map
 * @param <K2>	Value type of the first map, and key type of the second one
 * @param <V>	Value type of the second map
 */
public class LazyPipeMap<K1, K2, V> implements Map<K1, V> {
	private class IsFLKey implements Predicate<K1> {
		@Override
		public boolean appliesTo(K1 obj) {
			K2 l2Key = m1.get(obj);
			return l2Key != null && m2.containsKey(l2Key);
		}
	}
	
	private class CommonFirstLevelKeySet
			extends AbstractSet<K1> implements Set<K1> {

		@Override
		public Iterator<K1> iterator() {
			return Sequential.filter(m1.keySet(), new IsFLKey()).iterator();
		}

		@Override
		public int size() {
			return commonKeys().size();
		}
		
		@Override
		public boolean contains(Object o) {
			return containsKey(o);
		}

		@Override
		public boolean containsAll(Collection<?> c) {
			for (Object k : c) {
				if (!contains(k)) {
					return false;
				}
			}
			return true;
		}

		@Override
		public boolean removeAll(Collection<?> c) {
			boolean res = false;
			
			for (Object k : c) {
				res |= (LazyPipeMap.this.remove(k) != null);
			}
			
			return res;
		}

		@Override
		public boolean remove(Object o) {
			return LazyPipeMap.this.remove(o) != null;
		}

		@Override
		public boolean retainAll(Collection<?> c) {
			boolean res = false;
			for (Object k : this) {
				if (!c.contains(k)) {
					res |= remove(k);
				}
			}
			return res;
		}
	}

	private class ReachableValuesCollection
			extends AbstractCollection<V> implements Collection<V> {

		@Override
		public Iterator<V> iterator() {
			return Sequential.map(commonKeys(), new Mapper<K2, V>() {
				@Override
				public V map(K2 x) {
					return m2.get(x);
				}
			}).iterator();
		}

		@Override
		public int size() {
			return commonKeys().size();
		}

		@Override
		public boolean remove(Object o) {
			for (K2 k : commonKeys()) {
				if (m2.get(k).equals(o)) {
					m2.remove(k);
					m1.values().removeAll(Arrays.asList(k));
					return true;
				}
			}
			return false;
		}

		@Override
		public boolean removeAll(Collection<?> c) {
			boolean res = false;
			for (Object k : commonKeys().toArray()) {
				if (c.contains(m2.get(k))) {
					m2.remove(k);
					m1.values().removeAll(Arrays.asList(k));
					res = true;
				}
			}
			return res;
		}

		@Override
		public boolean retainAll(Collection<?> c) {
			boolean res = false;
			for (Object k : commonKeys().toArray()) {
				if (!c.contains(m2.get(k))) {
					m2.remove(k);
					m1.values().removeAll(Arrays.asList(k));
					res = true;
				}
			}
			return res;
		}
	}
	
	private class PipedEntry implements Map.Entry<K1, V> {
		private final K1 l1Key;
		private final K2 l2Key;
		private V value;
		
		private PipedEntry(K1 l1Key, K2 l2Key) {
			this.l1Key = l1Key;
			this.l2Key = l2Key;
			value = m2.get(l2Key);
		}
		
		@Override
		public K1 getKey() {
			return l1Key;
		}

		@Override
		public V getValue() {
			return value;
		}

		@Override
		public V setValue(V value) {
			this.value = value;
			return m2.put(l2Key, value);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + getOuterType().hashCode();
			result = prime * result + ((l1Key == null) ? 0 : l1Key.hashCode());
			result = prime * result + ((l2Key == null) ? 0 : l2Key.hashCode());
			result = prime * result + ((value == null) ? 0 : value.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			PipedEntry other = (PipedEntry) obj;
			if (!getOuterType().equals(other.getOuterType()))
				return false;
			if (l1Key == null) {
				if (other.l1Key != null)
					return false;
			} else if (!l1Key.equals(other.l1Key))
				return false;
			if (l2Key == null) {
				if (other.l2Key != null)
					return false;
			} else if (!l2Key.equals(other.l2Key))
				return false;
			if (value == null) {
				if (other.value != null)
					return false;
			} else if (!value.equals(other.value))
				return false;
			return true;
		}

		private LazyPipeMap getOuterType() {
			return LazyPipeMap.this;
		}
	}
	
	private class PipedEntrySet
			extends AbstractSet<Map.Entry<K1, V>>
			implements Set<Map.Entry<K1, V>> {

		@Override
		public Iterator<Map.Entry<K1, V>> iterator() {
			return Sequential.filter(Sequential.map(m1.entrySet(),
					new Mapper<Map.Entry<K1,K2>, Map.Entry<K1,V>>() {
				@Override
				public Map.Entry<K1, V> map(Map.Entry<K1, K2> x) {
					return new PipedEntry(x.getKey(), x.getValue());
				}
			}), new Predicate<Map.Entry<K1, V>>() {
				@Override
				public boolean appliesTo(Map.Entry<K1, V> obj) {
					return m2.containsKey(((PipedEntry)obj).l2Key);
				}
			}).iterator();
		}

		@Override
		public int size() {
			return commonKeys().size();
		}

		@Override
		public boolean removeAll(Collection<?> c) {
			boolean res = false;
			for (Object o : c) {
				res |= remove(c);
			}
			return res;
		}

		@Override
		public boolean remove(Object o) {
			PipedEntry ent = (PipedEntry)o;
			if (m1.containsKey(ent.l1Key) && m2.containsKey(ent.l2Key)) {
				m1.remove(ent.l1Key);
				m2.remove(ent.l2Key);
				return true;
			} else {
				return false;
			}
		}

		@Override
		public boolean retainAll(Collection<?> c) {
			boolean res = false;
			for (Map.Entry<K1, V> ent : this.toArray(new Map.Entry[0])) {
				if (!c.contains(ent)) {
					res |= remove(ent);
				}
			}
			return res;
		}
	}
	
	private final Map<K1, K2> m1;
	private final Map<K2, V> m2;
	
	/**
	 * Creates a LazyPipeMap
	 * @param first		First map
	 * @param second	Second map
	 */
	public LazyPipeMap(Map<K1, K2> first, Map<K2, V> second) {
		this.m1 = first;
		this.m2 = second;
	}
	
	/**
	 * Returns the set of common keys, which is the set of K2 items k2,
	 * such that <pre>m1.values().contains(k2) && m2.keySet().contains(k2)</pre> 
	 * @return	Set of K2 values
	 */
	private Set<K2> commonKeys() {
		Collection<K2> m1Vals = m1.values();
		Set<K2> m2Keys = new HashSet<>(m2.keySet());
		m2Keys.retainAll(m1Vals);
		return m2Keys;
	}
	
	@Override
	public int size() {
		return commonKeys().size();
	}

	@Override
	public boolean isEmpty() {
		return commonKeys().isEmpty();
	}

	@Override
	public boolean containsKey(Object key) {
		key = m1.get(key);
		if (key == null) {
			return false;
		} else {
			return m2.containsKey(key);
		}
	}

	@Override
	public boolean containsValue(Object value) {
		return m2.containsValue(value);
	}

	@Override
	public V get(Object key) {
		return m2.get(m1.get(key));
	}

	@Override
	public V put(K1 key, V value) {
		throw new UnsupportedOperationException();
	}

	@Override
	public V remove(Object key) {
		K2 k2 = m1.get(key);
		if (k2 != null) {
			V res = m2.remove(k2);
			if (res != null) {
				m1.remove(key);
			}
			return res;
		}
		return null;
	}

	@Override
	public void putAll(Map<? extends K1, ? extends V> m) {
		throw new UnsupportedOperationException();
	}

	@Override
	public void clear() {
		m1.clear();
		m2.clear();
	}

	@Override
	public Set<K1> keySet() {
		return new CommonFirstLevelKeySet();
	}

	@Override
	public Collection<V> values() {
		return new ReachableValuesCollection();
	}

	@Override
	public Set<java.util.Map.Entry<K1, V>> entrySet() {
		return new PipedEntrySet();
	}
}
